<?php

/*
 * 解析配置文件
 */
$config = parse_ini_file("./config.ini");
if (empty($config)) {
    die("配置文件错误");
}

/*
 * 校验
 */
$portStr = $config['cmpp_port'] ?? '';
if (empty($portStr)) {
    die("端口错误");
} else {
    $portArr = explode(",", $portStr);
}

$reportStr = $config['report'] ?? '';
if (empty($reportStr) || strlen($reportStr) != 7) {
    die("状态错误");
}

class Cmpp2
{

    public static $server = null;
    public static $atomit = null;

    const CMPP_CONNECT = 0x00000001;
    const CMPP_CONNECT_RESP = 0x80000001;
    const CMPP_TERMINATE = 0x00000002;
    const CMPP_TERMINATE_RESP = 0x80000002;
    const CMPP_SUBMIT = 0x00000004;
    const CMPP_SUBMIT_RESP = 0x80000004;
    const CMPP_DELIVER = 0x00000005;
    const CMPP_DELIVER_RESP = 0x80000005;
    const CMPP_QUERY = 0x00000006;
    const CMPP_QUERY_RESP = 0x80000006;
    const CMPP_CANCEL = 0x00000007;
    const CMPP_CANCEL_RESP = 0x80000007;
    const CMPP_ACTIVE_TEST = 0x00000008;
    const CMPP_ACTIVE_TEST_RESP = 0x80000008;
    const STATUS_SUCCESS = 0;
    const STATUS_STRUCTURE_ERROR = 1;
    //connect
    const STATUS_ILLEGAL_ADDR = 2;
    const STATUS_AUTH_ERROR = 3;
    const STATUS_VERSION_ERROR = 4;
    const STATUS_EXCEED_CON_NUM = 16;
    const STATUS_NOT_CMPP2_USER = 18;
    const STATUS_NO_USER = 19;
    const STATUS_PRICE_ERROR = 20;
    //submit
    const STATUS_INVALID_NUM = 100;
    const STATUS_EXCEED_LIMIT = 8;
    const STATUS_NOT_LOGIN = 102;
    const STATUS_INS_BALANCE = 103;
    const STATUS_PRODUCT_LOCKED = 101;
    const STATUS_PRODUCT_TYPE_LOCKED = 105;

    public $headerPackRule = 'NTotal_Length/NCommand_Id/NSequence_Id'; //头部解析规则
    public $bodyPackRule = [//协议体解析规则
        self::CMPP_CONNECT => 'a6Source_Addr/a16AuthenticatorSource/CVersion/NTimestamp',
        self::CMPP_CONNECT_RESP => '',
        self::CMPP_TERMINATE => '',
        self::CMPP_TERMINATE_RESP => '',
        self::CMPP_SUBMIT => 'a8Msg_Id/CPk_total/CPk_number/CRegistered_Delivery/CMsg_level/a10Service_Id/CFee_UserType/a21Fee_terminal_Id/CTP_pId/CTP_udhi/CMsg_Fmt/a6Msg_src/a2FeeType/a6FeeCode/a17ValId_Time/a17At_Time/a21Src_Id/CDestUsr_tl/a21Dest_terminal_Id/CMsg_Length/',
        self::CMPP_SUBMIT_RESP => '',
        self::CMPP_DELIVER => 'a8Msg_Id/',
        self::CMPP_DELIVER_RESP => 'JMsg_Id/CResult',
        self::CMPP_QUERY_RESP => '',
        self::CMPP_QUERY => '',
        self::CMPP_CANCEL => '',
        self::CMPP_CANCEL_RESP => '',
        self::CMPP_ACTIVE_TEST => '',
        self::CMPP_ACTIVE_TEST_RESP => 'CReserved',
    ];
    public $bodyLen = [//协议体长度
        self::CMPP_CONNECT => 27, //6 16 1 4
        self::CMPP_CONNECT_RESP => 18, // 1 16 1
        self::CMPP_TERMINATE => 0,
        self::CMPP_TERMINATE_RESP => 0,
        self::CMPP_SUBMIT => 147, //8 1 1 1 1 10 1 21 1 1 1 6 2 6 17 17 21 1 21 1 8 ！！未包含短信长度，因为该长度可变
        self::CMPP_SUBMIT_RESP => 9, //8 1
        self::CMPP_DELIVER => 0,
        self::CMPP_DELIVER_RESP => 9, //8 1
        self::CMPP_QUERY_RESP => 0,
        self::CMPP_QUERY => 0,
        self::CMPP_CANCEL => 0,
        self::CMPP_CANCEL_RESP => 0,
        self::CMPP_ACTIVE_TEST => 0,
        self::CMPP_ACTIVE_TEST_RESP => 1, //1
    ];
    public $errorList = [
        self::CMPP_CONNECT => [
            self::STATUS_STRUCTURE_ERROR => '消息结构错',
            self::STATUS_ILLEGAL_ADDR => '非法源地址',
            self::STATUS_AUTH_ERROR => '认证错',
            self::STATUS_VERSION_ERROR => '版本太高',
            self::STATUS_EXCEED_CON_NUM => '用户的连接数超出限制',
            self::STATUS_NOT_CMPP2_USER => '该用户配置的协议为HTTP,不支持使用CMPP协议登录',
            self::STATUS_NO_USER => '未获取到用户信息',
            self::STATUS_PRICE_ERROR => '单价设置错误',
        ],
        self::CMPP_CONNECT_RESP => [],
        self::CMPP_TERMINATE => [],
        self::CMPP_TERMINATE_RESP => [],
        self::CMPP_SUBMIT => [
            self::STATUS_PRODUCT_LOCKED => '产品被锁定',
            self::STATUS_PRODUCT_TYPE_LOCKED => '产品类型被锁定',
            self::STATUS_INVALID_NUM => '长号码无效',
            self::STATUS_EXCEED_LIMIT => '流量超限',
            self::STATUS_NOT_LOGIN => '用户未登录',
            self::STATUS_INS_BALANCE => '余额不足',
        ],
        self::CMPP_SUBMIT_RESP => [],
        self::CMPP_DELIVER => [],
        self::CMPP_DELIVER_RESP => [],
        self::CMPP_QUERY_RESP => [],
        self::CMPP_QUERY => [],
        self::CMPP_CANCEL => [],
        self::CMPP_CANCEL_RESP => [],
        self::CMPP_ACTIVE_TEST => [],
        self::CMPP_ACTIVE_TEST_RESP => [],
    ];
    public $status = self::STATUS_SUCCESS; //协议状态
    public $error; //协议错误
    public $needCloseFd = false; //是否需要关闭连接
    public $response; //协议响应
    protected $commandId; //协议动作
    protected $headerBinary; //协议头
    protected $bodyBinary; //协议头
    protected $headerArr; //解析后的协议头
    protected $bodyArr; //解析后的协议头
    protected $msgHexId; //msg id的十六进制字符串表现
    public $msgIdDecArr; //十进制msgid数组

    public function setBinary(string $binary)
    {
        $this->headerBinary = substr($binary, 0, 12);
        $this->bodyBinary = substr($binary, 12);
    }

    /**
     * parseHeader 解析数据头部获取协议动作
     * @return bool
     */
    public function parseHeader()
    {
        $this->headerArr = @unpack($this->headerPackRule, $this->headerBinary);

        $this->commandId = isset($this->headerArr['Command_Id']) ? $this->headerArr['Command_Id'] : null;

        if (empty($this->commandId)) {
            throw new Exception('协议头结构错误');
        }

        return true;
    }

    /**
     * parseBody 解析协议体
     * @return bool
     */
    public function parseBody()
    {
        //协议未设置commandId 或者 commandId不在给定的范围内
        if (!isset($this->commandId) || !isset($this->bodyPackRule[$this->commandId])) {
            throw new Exception('协议头CommandId错误');
        }

        //拆除连接和客户端探活操作无协议体
        if ($this->commandId === self::CMPP_TERMINATE || $this->commandId === self::CMPP_ACTIVE_TEST) {
            return true;
        }

        //是否有解包格式 如果全部完成则该步骤删除
        if ($this->bodyPackRule[$this->commandId] === '') {
            throw new Exception('暂未开放此CommandId');
        }

        switch ($this->commandId) {
            case Cmpp2::CMPP_SUBMIT:
                return $this->parseSubmitBody();
                break;
            default:
                return $this->parseCommonBody();
                break;
        }
    }

    /**
     * parseSubmitBody 解析submit协议体 此协议体是变长
     * @return bool
     */
    public function parseSubmitBody()
    {
        //协议体长度
        $bodyBinaryLen = strlen($this->bodyBinary);

        if ($bodyBinaryLen <= $this->bodyLen[self::CMPP_SUBMIT]) {
            //设置错误码和错误信息
            $this->setErrorStatus(self::STATUS_STRUCTURE_ERROR);

            //设置返回的数据包
            $this->packageStructureErrResp();

            return false;
        }

        //短信长度等于协议提总长-扣除短信的协议提长
        $msgContentLength = $bodyBinaryLen - $this->bodyLen[self::CMPP_SUBMIT];

        //解析协议体
        $this->bodyArr = @unpack($this->bodyPackRule[$this->commandId] . 'a' . $msgContentLength . 'Msg_Content/a8Reserve', $this->bodyBinary);

        if ($this->bodyArr === false) {
            throw new Exception('协议解析错误');
        }

        return true;
    }

    /**
     * parseCommonBody 解析普通协议体
     * @return bool
     */
    public function parseCommonBody()
    {
        //如果协议体长度和规定的不一致则返回
        if ($this->bodyLen[$this->commandId] !== strlen($this->bodyBinary)) {
            //设置错误码和错误信息
            $this->setErrorStatus(self::STATUS_STRUCTURE_ERROR);

            //设置返回的数据包
            $this->packageStructureErrResp();

            return false;
        }

        $this->bodyArr = @unpack($this->bodyPackRule[$this->commandId], $this->bodyBinary);

        if ($this->bodyArr === false) {
            throw new Exception('协议解析错误');
        }

        return true;
    }

    /**
     * getCommandId 获取协议动作
     * @return string
     */
    public function getCommandId()
    {
        return $this->commandId;
    }

    /**
     * getNeedCloseFd
     * @return bool
     */
    public function getNeedCloseFd()
    {
        return $this->needCloseFd;
    }

    /**
     * getResponse 获取响应数据
     * @return string
     */
    public function getResponse()
    {
        return $this->response;
    }

    /**
     * getHeader 获取协议头
     * @param string $key
     * @param string $default
     * @return array|string
     */
    public function getHeader(string $key = '', string $default = '')
    {
        if (empty($key)) {
            return $this->headerArr;
        }

        return isset($this->headerArr[$key]) ? $this->headerArr[$key] : $default;
    }

    /**
     * getBody 获取协议体
     * @param string $key
     * @param $default
     * @return array|string
     */
    public function getBody(string $key = '', $default = '')
    {
        if (empty($key)) {
            return $this->bodyArr;
        }

        if (isset($this->bodyArr[$key])) {
            return $this->bodyArr[$key];
        }

        return $default;
    }

    /**
     * getStatus 获取协议返回状态
     * @return int
     */
    public function getStatus()
    {
        return $this->status;
    }

    /**
     * getMsgHexId 获取十六进制的msg id
     * @return mixed
     */
    public function getMsgHexId()
    {
        return $this->msgHexId;
    }

    /**
     * setErrorStatus 设置错误
     * @param int $status
     */
    public function setErrorStatus(int $status)
    {
        $this->status = $status;

        $this->error = $this->errorList[$this->commandId][$status];
    }

    public function handleSubmit()
    {
        //获取msgid二进制字符串
        [$this->msgIdDecArr, $hexArr] = self::generateMsgIdArr($this->getBody('Msg_src'));

        $this->msgHexId = implode('', $hexArr);

        return true;
    }

    /**
     * handle 处理协议
     * @return bool
     * @throws Exception
     */
    public function handle()
    {
        switch ($this->commandId) {
            case self::CMPP_CONNECT:
                //客户端提交的连接请求
                return $this->handleConnect();
            case self::CMPP_SUBMIT:
                //客户端提交的发送连接请求
                return $this->handleSubmit();
            case self::CMPP_DELIVER_RESP:
                //客户端返回上行包
                return true;
            case self::CMPP_TERMINATE:
                //客户端提交的断开连接请求
                return $this->handleTerminate();
            case self::CMPP_ACTIVE_TEST:
                //客户段提交的探活请求
                return $this->handleActiveTest();
            case self::CMPP_ACTIVE_TEST_RESP:
                //客户端发返回的探活Resp包,不做处理
                return true;
            default:
                throw new Exception('协议头结构错误');
                break;
        }
    }

    /**
     * handleConnect 处理连接
     * @return bool
     * @throws Exception
     */
    public function handleConnect()
    {
        $spId = trim($this->getBody('Source_Addr'));

        //获取用户约定好的密码，用于cmpp2 auth校验和打包回执
        $sharedSecret = $this->getBody('password');

        //将传输过来的二进制数据转换成16进制，用于cmpp2 auth校验和打包回执
        $authenticatorSource = bin2hex($this->getBody('AuthenticatorSource'));

        //二进制转换会丢失首部的0 需要补上
        $timeStamp = str_pad((string) $this->getBody('Timestamp'), 10, '0', STR_PAD_LEFT);

        //打包响应
        $this->packageConnectResp($authenticatorSource, $sharedSecret);

        return true;
    }

    /**
     * handleDeliverResp
     * @return bool
     */
    public function handleDeliverResp()
    {
//        $submitMsgId = dechex($this->getBody('Msg_Id'));
//
//        $submitMsgId = str_pad($submitMsgId, 16, '0', STR_PAD_LEFT);
//
        return true;
    }

    /**
     * handleTerminate 处理客户端的断开连接请求
     * @return bool
     */
    public function handleTerminate()
    {
        $this->packageTerminateResp();

        return true;
    }

    /**
     * handleActiveTest 处理客户端探活
     * @return bool
     */
    public function handleActiveTest()
    {
        //如果客户端主动发送探活则重置探活失败次数和更新探活时间，避免服务端再次发送探活

        $this->packageActiveTestResp();

        return true;
    }

    /**
     * packageStructureErrResp 打包结构体错误的响应
     */
    public function packageStructureErrResp()
    {
        switch ($this->commandId) {
            case self::CMPP_CONNECT:
                $this->packageConnectResp('', '');
                break;
            case self::CMPP_SUBMIT:
                $this->getNoUserSessMsgIdArr();

                $this->packageSubmitResp();
                break;
        }
    }

    /**
     * packageConnectResp 打包连接响应数据
     * @param $authenticatorSource
     * @param $sharedSecret
     */
    public function packageConnectResp($authenticatorSource, $sharedSecret)
    {
        //生成AuthenticatorISMG
        $authenticatorISMG = md5($this->getStatus() . $authenticatorSource . $sharedSecret, true);

        //生成响应体
        $respBodyBinary = pack('Ca16C', $this->getStatus(), $authenticatorISMG, 0x20);

        //生成响应头
        $respHeaderBinary = pack('NNN', strlen($respBodyBinary) + 12, self::CMPP_CONNECT_RESP, $this->getHeader('Sequence_Id') ?? '');

        $this->response = $respHeaderBinary . $respBodyBinary;
    }

    /**
     * packageSubmitResp
     * TODO 放到扩展里面做提高性能
     */
    public function packageSubmitResp()
    {
        $this->msgIdDecArr[] = $this->getStatus();
        //生成响应体
        $respBodyBinary = pack('C8C', ...$this->msgIdDecArr);

        //生成响应头
        $respHeaderBinary = pack('NNN', strlen($respBodyBinary) + 12, self::CMPP_SUBMIT_RESP, $this->getHeader('Sequence_Id') ?? '');

        $this->response = $respHeaderBinary . $respBodyBinary;
    }

    /**
     * packageTerminateResp 设置erminate响应数据
     */
    public function packageTerminateResp()
    {
        $this->response = pack('NNN', 12, self::CMPP_TERMINATE_RESP, $this->getHeader('Sequence_Id'));
    }

    /**
     * packageActiveTestResp 设置探活响应数据
     */
    public function packageActiveTestResp()
    {
        $respBodyBinary = pack('c', '0');

        $respHeaderBinary = pack('NNN', strlen($respBodyBinary) + 12, self::CMPP_ACTIVE_TEST_RESP, $this->getHeader('Sequence_Id'));

        $this->response = $respHeaderBinary . $respBodyBinary;
    }

    /**
     * generateMsgIdArr 生成msgid二进制字符串，转换成八位的数组
     * @param $srcNumber
     * @return array
     * TODO 放到扩展里面做提高性能
     */
    public static function generateMsgIdArr($srcNumber)
    {
        $msgSequenceId = self::generateMsgSequenceId();

        $dateStr = date('mdHis');

        $dateArr = str_split($dateStr, 2);

        //转换成二进制字符串
        $msgIdStr = sprintf("%04s%05s%05s%06s%06s%022s%016s", decbin($dateArr[0]), decbin($dateArr[1]), decbin($dateArr[2]), decbin($dateArr[3]), decbin($dateArr[4]), decbin($srcNumber), decbin($msgSequenceId));



        //分割字符串为8位一组
        $msgIdBinary = str_split($msgIdStr, 8);

        //将二进制转换为十进制因为pack只认字符串10进制数为十进制数
        $decArr = []; //十进制
        $hexArr = []; //十六进制
        foreach ($msgIdBinary as $binary) {
            $dec = bindec($binary);
            $decArr[] = $dec;
            $hexArr[] = str_pad(dechex($dec), 2, '0', STR_PAD_LEFT);
        }

        return [$decArr, $hexArr];
    }

    /**
     * parseMsgIdToDecArr 将十六进制的msgId转换成十进制的八份数据
     * @param $msgId
     * @return mixed
     */
    public static function parseMsgIdToDecArr($msgId)
    {
        //msgId是个十六进制的16字节数据,将16字节按2字节一份分成八份，如果按照16进制算其实两份才是一个字节
        $hexMsgIdArr = str_split($msgId, 2);

        $msgDecArr = [];

        foreach ($hexMsgIdArr as $byte) {
            $msgDecArr[] = hexdec($byte);
        }

        return $msgDecArr;
    }

    /**
     * generateMsgSequenceId 获取msg的sequence id
     * @return int
     */
    protected static function generateMsgSequenceId()
    {
        $sequenceId = Cmpp2::$atomit->add();
        return $sequenceId % 65535;
    }

    /**
     * generateProSequenceId 生成协议的sequence id
     * @return int
     */
    protected static function generateProSequenceId()
    {
        $sequenceId = Cmpp2::$atomit->add();
        return $sequenceId % 65535;
    }

    /**
     * getNoUserSessMsgIdArr 获取没有sess的情况下的msgid数组
     */
    public function getNoUserSessMsgIdArr()
    {
        //获取msgid二进制字符串
        [$this->msgIdDecArr, $hexArr] = self::generateMsgIdArr('');

        $this->msgHexId = implode('', $hexArr);
    }

    /**
     * sendTerminate 发送拆除连接信息
     * @param $fd
     * @return mixed
     */
    public static function sendTerminate($fd)
    {
        //获取序列ID
        $sequenceId = self::generateProSequenceId();

        //拼装头信息
        $pack = pack('NNN', 12, self::CMPP_TERMINATE, $sequenceId);

        //发送拆除连接信息
        return Cmpp2::$server->send($fd, $pack);
    }

    /**
     * sendActiveTest
     * @param $fd
     * @return mixed
     */
    public static function sendActiveTest($fd)
    {
        //获取序列ID
        $sequenceId = self::generateProSequenceId();

        //拼装头信息
        $pack = pack('NNN', 12, self::CMPP_ACTIVE_TEST, $sequenceId);

        //发送链路检测
        return Cmpp2::$server->send($fd, $pack);
    }

    /**
     * prepareReportBinary
     * @param $upStreamMsgId
     * @param $spId
     * @param $userExt
     * @param $mobile
     * @param $stat
     * @param string $submitTime
     * @param string $doneTime
     * @return array
     */
    public static function prepareReportBinary($upStreamMsgIdDecArr, $userExt, $mobile, $stat, $submitTime = '', $doneTime = '')
    {

        $upStreamMsgIdBinary = @pack('C8', ...$upStreamMsgIdDecArr);

        $time = date('ymdHi');

        $submitTime = $submitTime ?: $time;

        $doneTime = $doneTime ?: $time;

        $msgContentBinary = $upStreamMsgIdBinary . pack('a7a10a10a21N', $stat, $submitTime, $doneTime, $mobile, 0);

        $msgLength = 57;

        $bodyBinary = $upStreamMsgIdBinary . pack('a21a10CCCa21CCa' . $msgLength . 'a8', $userExt, '', 0, 0, 0, $mobile, 1, $msgLength, $msgContentBinary, '');
        $headerBinary = pack('NNN', 12 + strlen($bodyBinary), self::CMPP_DELIVER, self::generateProSequenceId());

        return $headerBinary . $bodyBinary;
    }

    public static function respAndClose($server, $fd, Cmpp2 $protocol)
    {
        //如果fd存在，并且不是接收的响应包则反回
        if ($server->exist($fd) && !in_array($protocol->getCommandId(), [
                    Cmpp2::CMPP_ACTIVE_TEST_RESP,
                    Cmpp2::CMPP_TERMINATE_RESP,
                    Cmpp2::CMPP_DELIVER_RESP
                ])) {
            //发送响应
            $server->send($fd, $protocol->getResponse());

            //如果需要关闭连接或是客户端的terminate操作则关闭连接
            if ($protocol->getNeedCloseFd() || $protocol->getCommandId() === Cmpp2::CMPP_TERMINATE) {
                if (!$server->close($fd)) {
                    Log::warning('【协议出错】客户端fd：' . $fd . '关闭失败，请确认');
                }
            }
        }
    }

}

/*
 * 初始化swoole类
 * port填0 主服务获取一个系统随机可用端口
 */
$server = new Swoole\Server('0.0.0.0', 0, SWOOLE_BASE);

Cmpp2::$server = $server;

Cmpp2::$atomit = new Swoole\Atomic();

$server->set([
    'worker_num' => swoole_cpu_num(),
    'enable_coroutine' => false
        ]
);

//监听连接进入事件
$server->on('Connect', function ($server, $fd) {
    echo "Client: Connect.\n";
});


//监听连接关闭事件
$server->on('Close', function ($server, $fd) {
    echo "Client: Close.\n";
});


$server->on('receive', function () {
    echo "Recv\n";
});




/*
 * 多端口监听 的OnRecive回调
 * 同时提供多个CMPP服务
 */

function realRecv($server, $fd, $from_id, $data)
{

    global $reportStr;

    $protocol = new Cmpp2();

    $protocol->setBinary($data);

    //解析协议头
    $protocol->parseHeader();

    if ($protocol->getCommandId() === Cmpp2::CMPP_ACTIVE_TEST_RESP) {
        //如果是客户端反回的探活包回应则不做处理
        return;
    }

    //解析协议体成功了，执行后续操作
    if ($protocol->parseBody()) {
        $handleRes = $protocol->handle();
    }

    if ($protocol->getCommandId() === Cmpp2::CMPP_SUBMIT && $handleRes) {
        $protocol->packageSubmitResp();
    }

    $submitTime = date('ymdHi');

    //发送submit resp同时确认是否关闭连接
    Cmpp2::respAndClose($server, $fd, $protocol);

    /*
     * 发送状态给客户端
     */
    if ($server->exist($fd) && $protocol->getCommandId() === Cmpp2::CMPP_SUBMIT) {
        //base模式+sleep可以模拟缓冲区写满
//        sleep(100000000);
        $doneTime = date('ymdHi');
        $body = $protocol->getBody();
        $reportBinary = Cmpp2::prepareReportBinary($protocol->msgIdDecArr, $body['Src_Id'], $body['Dest_terminal_Id'], $reportStr, $submitTime, $doneTime);
        $server->send($fd, $reportBinary);
    }
}

$portObj = [];
foreach ($portArr as $k => $value) {
    $portObj[$k] = $server->listen('0.0.0.0', $value, SWOOLE_SOCK_TCP);
    $portObj[$k]->set([
        'open_length_check' => true,
        'open_tcp_nodelay' => true,
        'package_length_type' => 'N',
        'package_length_offset' => 0,
        'package_body_offset' => 0,
    ]);
    $portObj[$k]->on('receive', 'realRecv');
}


echo("\n\033[32mCMPP模拟器启动成功，监听端口 {$portStr},返回状态 {$reportStr}\033[0m\n\n");

//启动服务器
$server->start();
